--[[--------------------------------------------------
AutocompleteObject.lua
mozers? Tymur Gubayev
version 3.10.11
------------------------------------------------------
Inputting of the symbol set in autocomplete.[lexer].start.characters causes the popup list of properties and methods of input_object. They undertake from corresponding api-file.
In the same case inputting of a separator changes the case of symbols in input_object's name according to a api-file.
(for example "ucase" is automatically replaced on "UCase".)

Warning: This script needed function IsComment and string.pattern (COMMON.lua)
props["APIPath"] available only in SciTE-Ru

Connection:
In file SciTEStartup.lua add a line:
  dofile (props["SciteDefaultHome"].."\\tools\\AutocompleteObject.lua")
Set in a file .properties:
  autocomplete.lua.start.characters=.:

-------------------------------------------------- ----
To understand the algorithm of the script, we agree that the record
azimuth: left; list-style-| type: upper-roman
where the cursor is at the position marked with "|", and the symbol "-" is one of the separators, part
list-style - will be called "object"
type - will be called the "method"
All the above applies to all programming languages ​​(css here - just an example)
The script will only work with the "right" api files (see format in ActiveX.api)
-------------------------------------------------- ----
Tip:
If, after the separator list of properties and methods did not appear (although they are described in the api file)
it is possible that the script does not recognize the name of your object.
Help him in this by writing the following line in the edited document:
mydoc = document
where mydoc - the name of your facility
document - the name of the same object, set in api file
-------------------------------------------------- ----
History:
3.10.7 (mozers):
        - To attach event handlers SciTE now used p-tion of AddEventHandler COMMON.lua
3.10.6 (Tymur):
        - The function is called FindDeclaration now only at the opening and after saving the file, which speeds up the script for large files.
3.10.4 (mozers):
        - Fixed bug with drop-down list to reveal a single line
3.10.3 (Tymur):
        - The conversion function in Thong pattern search is moved to COMMON.lua
3.0 (Tymur):
        - The script was a little more case sensitive than the old
        * Rewrote table objects_table, alias_table array of rows to tables with string keys
        * Simultaneously performed tasks 1 and 2 in the list above
        + Added feature for lua: recognition of string variables in the code (associated with the table string)
        + Added in beta mode to detect a slightly different object names. See new_delim_behavior
        * FindDeclaration () looks for determination in accordance with the language: Acceptable characters are taken from $ (word.characters. $ (File.patterns.LANGUAGE)) $ (autocomplete.LANGUAGE.start.characters)
        * In general, the script should run faster (especially after the first call), by combining the performance of old CreateObjectsTable () and CreateAliasTable () in a single pass, and only if necessary (get_api == true)
        * Fixed old bug that is described in the message VoidVolker: http://forum.ru-board.com/topic.cgi?forum=5&topic=24956&start=840 # 5
        Tested only for Lua, now it works :)
        + If api-file lines of the form are found Lua: t ["a-b + c"] \ nThis very tricky option
          it makes sense to add autocomplete.lua.start.characters character '['
3.01 (Tymur):
        + Script again-insensitive (see "-" version 3.0)
        + Automatically corrected sensitive entered the object name (the word to the left of the separator), pointed in the api-file (for Lua: StriNG will string), but only when using the auto-complete.
3.05 (Tymur):
        * Fix register occurs just after entering the separator
        + To the numerous requests of workers now plays the role new_delim_behavior props ["autocomplete.object.alt"] == "1", so that those who want to enjoy the new algorithm should prescribe acc. setting.
        * Fixed a new algorithm, when it did not work for *. Css (and other) files (Found mozers). Now you can from word.characters. *. Css remove "-" symbol.
        * Improved backward compatibility of the new algorithm: in the absence of the object name to the long version (new algorithm) using the short name (without separators) (old algorithm).
3.06 (mozers):
        * Parameter and variable autocomplete.object.alt new_delim_behavior_better_buggy replaced by a single parameter autocomplete.object.method = 0 | 1 | 2 from which you can select one of the three algorithms.
3.07 (mozers):
        - Parameter autocomplete.object.method now deprecated. The processing is one algorithm.
        * The GetInputObject rewritten. Use revision [GetWordChars] BioInfo.
3.08 (mozers):
        * Search the text and object definitions strings isolated in separate procedures and IsObject IsString
        * Now in lua methods of the string are displayed after a point, and similar methods of string variables - after: (required revision SciTELua.api)
3.09 (Tymur):
        - IsString abolished as unnecessary
        + Now. Api files, only the design of the form # $ string_value = ^ '. *' And even # $ file = io.open% b (). Part of the right of the "=" sign - a pattern that is used for object detection. Thus possible to extend the functionality of features for recognition of strings of all that is caught Lua regular expressions.
        ! script itself no longer recognizes string objects! Necessary corrections. Api-files:
                For example, at the end lua.api put the following lines:
                        # $ String_value = ^ ". *"
                        # $ String_value = ^ '. *'
                        # $ String_value = ^% [% [. *%]%]
3.10 (Tymur):
        * Fixed a minor bug where some objects are not recognized ("azimuth: right-" did not call list "side")
        * Fixed a pattern recognition method (the effect is noticeable only if autocomplete.lexer.start.characters intersect word.characters.lexer, for example, css)
3.10.2 (Tymur):
        * Another patch for css (drop-down list will no longer appear bad items, compare. "Text-" before and after the patch)屐屙螓, 耩噔? "text-"漕 ?镱耠?镟蝼?
--]]----------------------------------------------------

local current_pos = 0    -- current cursor position, it is important to InsertMethod
local sep_char = ''      -- keystrokes (in our case - one of the separators ".")
local auto_start_chars_patt = '' -- pattern containing escapes from the parameter autocomplete.lexer.start.characters
local get_api = true     -- specifies the need to reload the file api
local api_table = {}     -- all lines api file (remove any unnecessary information we)
local objects_table = {} -- the "objects" found in the api file as objects_table[objname]=true
local alias_table = {}   -- mapping "object = synonym"
local patterns_table = {}-- mapping "object = pattern to check the line"
local methods_table = {} -- all "methods" given "object" found in the api file
local object_names = {}  -- all names available api file "objects", which corresponds to the found in the current file "object"
local autocomplete_start_characters = '' -- delimiter character objects (from the parameter autocomplete.lexer.start.characters)
local object_good_name = '' -- "good" name of the object as it appears in the api-file, case sensitive
local word_chars_patt = ''

------------------------------------------------------
-- Table is sorted alphabetically and removes duplicates
local function TableSort(table_name)
	table.sort(table_name, function(a, b) return a:upper() < b:upper() end)
	-- remove duplicates
	for i = #table_name-1, 0, -1 do
		if table_name[i] == table_name[i+1] then
			table.remove (table_name, i+1)
		end
	end
	return table_name
end

------------------------------------------------------
-- Table is sorted alphabetically and removes duplicates
-- i.e. introduced the "object" wShort, and methods will be sought for WshShortcut  and WshURLShortcut
local function GetObjectNames(text)
	local TEXT = text:upper()
	local obj_names = {}
	-- Search by name table "objects"
	if objects_table[TEXT] then
		obj_names[1] = objects_table[TEXT]
		return obj_names, objects_table[TEXT] -- If successful, the complete search
	end

	-- Search the mapping table "object - a synonym for"
	if alias_table[TEXT] then
		for _,v in pairs(alias_table[TEXT]) do obj_names[#obj_names+1] = v end
		return obj_names , (alias_table[TEXT] and alias_table[TEXT]())
	end

	-- Checking on patterns:
	for obj_name, patterns in pairs(patterns_table) do
		for _, patt in ipairs(patterns) do
			if text:find(patt) then
				obj_names[#obj_names+1] = obj_name
			end
		end	
	end
	return obj_names , nil -- if found on the pattern, there is no "correct" name of the object for which it was possible to correct the entered
end

------------------------------------------------------
-- Extract from the current file name of the object to which "work":
-- We take the "word" to the left of the cursor, and is considered a symbol autocomplete.start.characters part of the word
local function GetInputObject()
	local word_chars = props['CurrentWordCharacters']
	-- add separators - they are now also part of the word
	editor.WordChars = word_chars..autocomplete_start_characters

	-- If the separator is after the braces - we take the word to the left of the brackets with them.
	-- need for advanced-aliasing patterns
	local word_start_pos = editor:BraceMatch(current_pos-2)
	if word_start_pos == -1 then word_start_pos = current_pos end
	-- for let us do all the work editor:WordStartPosition
	word_start_pos = editor:WordStartPosition(word_start_pos-1)
	-- return the settings back
	editor.WordChars = word_chars
	return editor:textrange(word_start_pos, current_pos-1)
end

------------------------------------------------------
-- returns an empty table function that returns the "correct" name
local function CreateAliasEntry(obj)
	return setmetatable({}, { __call = function () return obj end })
end

------------------------------------------------------
-- add an alias to a table mapping "object - a synonym for"
local function AddAlias(alias, obj)
	local ALIAS = alias:upper()
	-- If the first time such a word, create a table
	alias_table[ALIAS] = alias_table[obj] or CreateAliasEntry(alias)
	-- add synonym alias to an object obj
	alias_table[ALIAS][obj:upper()] = obj
end

------------------------------------------------------
-- Posted pattern in the mapping table "object - a pattern"
local function AddPattern(obj, patt)
	-- If the first time such a word, create a table
	patterns_table[obj] = patterns_table[obj] or {}
	-- add a pattern to an object obj patt
	table.insert(patterns_table[obj], patt)
end

------------------------------------------------------
-- check whether the text of the definition of the object
local function IsObject(text)
	-- make a table of words to test. In the first place whole string - to check the pattern.
	local words_to_test = {text}
	for words in text:gmatch(word_chars_patt) do
		words_to_test[#words_to_test+1] = words
	end
	
	for _, sValue in ipairs(words_to_test) do
		local objects = GetObjectNames(sValue)
		for i = 1, #objects do
			-- next check is needed to recover really _imya_ object, not an alias.
			if objects_table[objects[i]:upper()] then
				return objects[i]
			end
		end
		--@debug: this just in case in case.
		if #objects > 0 then
			print('ATTENTION: object '..sValue..' has only aliases, and no correct name!\n\tSee function IsObject from AutocompleteObject.lua')
		end
	end --for words in text
	--return ''
end

------------------------------------------------------
-- Search returns a custom variable assignment of a real object
-- i.e. in the current file structure looking like "a synonym = object"
local function FindDeclaration()
	local text_all = editor:GetText()
	local _start, _end, sVar, sRightString
	-- take what is stored in, for example, word.characters.$(file.patterns.lua)
	word_chars_patt = '['..props['CurrentWordCharacters']:pattern()..auto_start_chars_patt..']+'

	-- @todo: the right side is also a good idea to tweak.
	local pattern = '('..word_chars_patt..')%s*=%s*(%C+)'
	_start = 1
	while true do
		_start, _end, sVar, sRightString = text_all:find(pattern, _start)
		if _start == nil then break end
		-- remove spaces at the beginning / end
		sRightString = sRightString:gsub("^%s*(%S*)%s*$", "%1")
		if sRightString ~= '' then
			-- analiziruem text floors enabled "=" sign
			-- check, not if it contains an object described in the api?
			local obj = IsObject(sRightString)
			-- if indeed, this "object" exists, then add it to the mapping table "object - a synonym for"
			if obj then AddAlias(sVar, obj) end
		end
		_start = _end + 1
	end
end

------------------------------------------------------
-- Reading api file to a table api_table (so you do not poll the drive, and pull all of it)
local function CreateAPITable()
	api_table = {}
	local word_patt = props['CurrentWordCharacters']:pattern()
	local word_extended_patt = '['..word_patt..auto_start_chars_patt..']'
	for api_filename in props["APIPath"]:gmatch("[^;]+") do
		if api_filename ~= nil then
			local api_file = io.open(api_filename)
			if api_file then
				for line in api_file:lines() do
					-- cut off the comments, leaving the alias patterns unchanged
					line = line:match('^#$%s*'..word_extended_patt..'+=.+$') or line:match('^[^%s%(]+')
					if line then
						api_table[#api_table+1] = line
					end
				end
				api_file:close()
			end
		end
	end
	get_api = false
	return false
end

------------------------------------------------------
-- Create a table containing all the names of "objects" described in the api file
-- Create a table containing all matching "synonym = object" described in the api file
local function CreateObjectsAndAliasTables()
	objects_table = {}
	alias_table = {}
	patterns_table = {}
	local word_patt = props['CurrentWordCharacters']:pattern()
	local word_extended_patt = '['..word_patt..auto_start_chars_patt..']'
	word_patt = '['..word_patt..']'

	for i = 1, #api_table do
		local line = api_table[i]
		-- It is extremely important that the match was exactly [auto_start_chars_patt], ie for example, "[.:]" for Lua
		-- Because This table is built only when api_get, can leave garbage.
		local obj_name = line:match('^[^#]') and line:match('^('..word_extended_patt..'*)['..auto_start_chars_patt..']')
		if obj_name then objects_table[obj_name:upper()]=obj_name end

		-- for lines of the form "# a = b" write a, b ​​to the table aliases
		local sObj, sAlias = line:match('^#('..word_extended_patt..'+)=(%S+)$') --@todo: think about the pattern ...
		if sObj then
			AddAlias(sAlias, sObj)
		end

		-- for strings like "# $ a = b" write a, b ​​to the table of patterns
		local sObj, sPatt = line:match('^#$%s*('..word_extended_patt..'+)=(.+)$') --@todo: think about the pattern ...
		if sObj then
			AddPattern(sObj, sPatt)
		end
	end
end

------------------------------------------------------
-- Create table "methods" given "object"
local function CreateMethodsTable(obj)
	for i = 1, #api_table do
		local line = api_table[i]
		-- look for lines that begin with the "object"
		local _start, _end = line:find(obj..sep_char, 1, true)
		if _end ~= nil and line:sub(1,1)~='#' and
			(_start == 1 or line:gsub(_start-1,_start-1)==sep_char) then
			-- local _start, _end, str_method = line:find('([^'..auto_start_chars_patt..']+)', _end+1)
			local _start, _end, str_method = line:find('(['..props['CurrentWordCharacters']:pattern()..']+)', _end+1)
			if _start ~= nil then
				methods_table[#methods_table+1] = str_method
			end
		end
	end
end

------------------------------------------------------
-- Show a drop-down list of "methods"
local function ShowUserList()
	if #methods_table == 0 then return false end
	local sep = '•' -- delimiter line drop-down list
	local methods_list = table.concat(methods_table, sep)
	if methods_list == '' then return false end
	local sep_tmp = editor.AutoCSeparator
	editor.AutoCSeparator = string.byte(sep)
	editor:UserListShow(7, methods_list)
	editor.AutoCSeparator = sep_tmp
	return true -- CUT further processing OnChar (avtokomplit etc.)
end

------------------------------------------------------
-- Inserts selected from the drop-down method in the edited row
AddEventHandler("OnUserListSelection", function(tp, sel_value)
	if tp == 7 then
		-- current_pos points where we were entering the separator,
		-- editor.CurrentPos Now - this is the beginning of the word to the left current_pos,
		-- sel_value contains the word separator + + that should be added.
		editor:SetSel(current_pos, editor.CurrentPos)
		editor:ReplaceSel(--[[object_good_name..]]sel_value)
	end
end)

-- Replaced with the name of the object to the user from the api file (for example, 'wscript' on 'WScript'))
local function CorrectRegisterSymbols(object_name)
	editor:SetSel(current_pos - #object_name, current_pos)
	editor:ReplaceSel(object_name)
end

-- The basic procedure (handle key press)
local function AutocompleteObject(char)
	if IsComment(editor.CurrentPos-2) then return false end  -- If the line is commented out, then exit

	autocomplete_start_characters = props["autocomplete."..props['Language']..".start.characters"]
	-- If the parameter autocomplete.lexer.start.characters empty, then exit
	if autocomplete_start_characters == '' then return false end

	-- workaround problems for endangered list
	-- if not spell out autocomplete.lexer.start.characters，то
	if not autocomplete_start_characters:find(char, 1, 1) then
		local word_start = editor:WordStartPosition(editor.CurrentPos)
		local leftchar = editor:textrange(word_start-1, word_start)
		-- If the left of the word still separator, return the status of the list: show / not shown.
		-- Thus, if the list is displayed, then all of the following handlers OnChar fail.
		return autocomplete_start_characters:find(leftchar, 1, 1) and editor:AutoCActive()
	end

	-- Finally we realized that the resulting character - exactly the divider!
	sep_char = char
	auto_start_chars_patt = autocomplete_start_characters:pattern()

	if get_api then
		CreateAPITable()
		CreateObjectsAndAliasTables()
		FindDeclaration() -- The mapping table object synonym recreated only after saving
	end
	-- if api_table empty - exit.
	if not next(api_table) then return false end

	-- Important: remember tekschuyu cursor position (Otherwise "string.b | [Enter]" will turn into "string.bbyte")
	current_pos = editor.CurrentPos

	-- We take as the object of the word to the left of the cursor, update current_pos the beginning word to the left of the cursor.
	local input_object = GetInputObject(autocomplete_start_characters)

	-- If the left of the cursor is not a word that can be interpreted as the object name, then exit
	if input_object == '' then return false end

	object_names, object_good_name = GetObjectNames(input_object)
	if object_good_name and input_object ~= object_good_name then
		CorrectRegisterSymbols(object_good_name..char)
	end

	-- Exit if the word is not the name of the object
	if not next(object_names) then return false end

	-- kill the remnants of the old methods, filled with new
	methods_table = {}
	for i = 1, #object_names do
		CreateMethodsTable(object_names[i])
	end
	methods_table = TableSort(methods_table)
	return ShowUserList()
end

------------------------------------------------------
AddEventHandler("OnChar", AutocompleteObject)

AddEventHandler("OnSwitchFile", function()
	get_api = true
end)

AddEventHandler("OnOpen", function()
	get_api = true
end)

AddEventHandler("OnBeforeSave", function()
	get_api = true
end)
